using System.Collections.Generic;
using System.Diagnostics.Contracts;

namespace System.Threading.Tasks
{
    /// <summary>
    /// Can be passed to an async executing operation to report progress and partial results back to the caller.
    /// It takes care of routing the notifications back to caller's thread (e.g. UI thread).
    /// </summary>
    /// <remarks>Class requires <see cref="SynchronizationContext.Current"/> to be set, otherwise notification cannot be routed back
    /// to the calling thread. In order to use this from e.g. console applications or powershell, <see cref="AsyncPump"/> can be used.</remarks>
    /// <typeparam name="T">Result type to communicate results back</typeparam>
    /// <example><code><![CDATA[
    /// int progress = 0;
    /// List<string> results = new List<string>();
    /// IEnumerable<string> totalResult = null;
    /// 
    /// AsyncPump.Run(async delegate
    /// {
    ///     IProgressNotifier<IEnumerable<string>> notifier = new Notifier<IEnumerable<string>>((partialResults, prog) =>
    ///     {
    ///         progress = prog;
    ///         results.AddRange(partialResults);
    ///     });
    /// 
    ///     totalResult = OperationWithProgressNotifications(notifier);
    /// });
    /// 
    /// private IEnumerable<string> OperationWithProgressNotifications(IProgressNotifier<IEnumerable<string>> notifier)
    /// {
    ///     List<string> results = new List<string>();
    /// 
    ///     for (int i = 0; i < 10; i++)
    ///     {
    ///         string r = $"Result: {i}";
    ///         notifier.Notify(new[] {r}, (i+1)*10);
    ///         results.Add(r);
    ///     }
    /// 
    ///     return results;
    /// }
    /// ]]>
    /// </code></example>
    public sealed class Notifier<T> : IProgressNotifier<T>, IDisposable
    {
        private readonly SynchronizationContext _context;
        private readonly Action<T, int> _callback;
        private readonly CancellationTokenSource _source;

        private void InternalCB(object state)
        {
            var p = (KeyValuePair<T, int>)state;
            _callback(p.Key, p.Value);
        }

        /// <summary>
        /// Class requires <see cref="SynchronizationContext.Current"/> to be set, otherwise notification cannot be routed back
        /// to the calling thread.
        /// </summary>
        /// <param name="callback"></param>
        /// <exception cref="InvalidOperationException"></exception>
        public Notifier(Action<T, int> callback)
        {
            Contract.Requires<ArgumentNullException>(callback != null);

            _context = SynchronizationContext.Current;
            if (_context == null)
            {
                throw new InvalidOperationException("No SynchronizationContext set! Use AsyncPump from AppToolkit.Common!");
            }

            _callback = callback;

            _source = new CancellationTokenSource();
        }

        void IProgressNotifier<T>.Notify(T chunk, int progressPercentage)
        {
            KeyValuePair<T, int> state = new KeyValuePair<T, int>(chunk, progressPercentage);
            _context.Post(InternalCB, state);
        }

        /// <summary>
        /// Allows cancelling the async operation.
        /// </summary>
        public void CancelOperation()
        {
            _source.Cancel();
        }

        CancellationToken IProgressNotifier<T>.CancellationToken => _source.Token;

        #region Dispose Pattern
        private bool _disposed;

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                // if this is a dispose call dispose on all state you 
                // hold, and take yourself off the Finalization queue.
                if (disposing)
                {
                    _source.Dispose();
                }

                _disposed = true;
            }
        }
        #endregion
    }
}